Interfaces / Methods / VTables

  • The only reason I would organize data into a struct, instead of keeping them loose, would be POLYMORPHISM.

  • Ways to have different systems for the same type of data:

    1. Function Pointers inside the struct, with different implementations.

      • Reminds of methods, but:

        • The procedure is not private, nor anything.

        • Doesn't interact with any constructor or destructor.

        • Not part of any high-level abstraction concept.

        • Can be changed at runtime.

      • In other words, it's better than a method.

    2. ~Generics.

      • Doesn't solve the problem. I don't want a generic procedure, but a completely different implementation of a procedure.

    3. ~Procedure Overloading.

      • Doesn't solve the problem. I don't want a dynamic dispatcher that judges the object type and calls a different procedure.

  • Cases where this could be useful :

    • ECS :

      • Reminds me of ECS concepts, where I could use a struct and call the struct with its own procedure.

      • Probably not exactly ECS, but it allows for SOA usage.

    • Create of User_Character / NPC_Character / Creature.

    • Destroy of User_Character / NPC_Character / Creature.

    • PrePhysics of User_Character / NPC_Character / Creature.

    • PostPhysics of User_Character / NPC_Character / Creature.

    • Draw of User_Character / NPC_Character / Creature.

    • DrawCanvas of User_Character / NPC_Character / Creature.

    • Scene.

      • This could remove the use of switches for: init, deinit, input, physics, draw.

      • The same goes for change_scene.

    • Note :

      • Technically this can be used in many places, BUT, I should only use it if I feel there's value in polymorphism.

  • OOP in Odin .

    • "just function pointers with a fancy name".

    • Use of "function tables" ("V tables", virtual tables) with using  in structs to achieve inheritance.

Operator ->

  • Operator -> .

  • The ->  operator can be used to inject a pointer to itself as the first parameter of the procedure.

  • As the ->  operator is effectively syntactic sugar , all of the same semantics still apply, meaning subtyping through using  will still work as expected to allow for the emulation of type hierarchies.

  • The ->  syntax is being abused in the 2nd option to mimic UFCS , when it was initially implemented mostly for C++ Component Objective Model (COM)  pattern and Objective C code interop.

x->y(123)
// is equivalent to
x.y(x, 123)

Discussion

  1. Many procedures symbolizing init, deinit, update, and draw for a scene. Each scene holds an enum value to know which scene to play at a given moment. When switching to a different scene, I will have to use a switch  to properly call the deinit procedure for the last_scene , and use a switch  to call init for the new_scene .

    • Packing the abstraction into control flow.

  2. Many procedures symbolizing init, deinit, update, and draw for a scene, but the scene is now a struct holding function pointers to each of its systems. When switching to a different scene, I just call last_scene.deinit(last_scene)  or last_scene->deinit() , where deinit  is the function pointer.

    • Packing the abstraction into memory.

  3. You can have a [Scene_Enum]Deinit_Proc  that contains each deinit proc for each Scene, and just use the scene enum to call the proc.

    • Instead of attaching methods to types, group all operations by behavior.

    typedef void (*DrawFunc)(void *);
    
    DrawFunc draw_funcs[MAX_ENTITY_TYPE];
    
    draw_funcs[ENTITY_PLAYER] = PlayerDraw;
    draw_funcs[ENTITY_ENEMY]  = EnemyDraw;
    
  4. Subtype polymorphism with runtime type-safe down-casting .

    • Just a selector with Enum.

  • Performance :

    • I got a bit worried that option 2 could be bad for a system like a scene or entity, as these function pointers are called EVERY FRAME, for every entity (in cases where I use stuff like this for entities). Am I overthinking performance here? I mean, C++ seems to do that in the end, so is it a bad thing for performance?

    • Opinions :

      • I've seen all of them in practice, and they really are nothing but syntactic choices. I doubt they'd impact performance that much. But that is just my uneducated guess.

      • Depends on how the CPU behaves, best to benchmark if you really care although I doubt it will matter much. Also an uneducated guess.

      • Source .

      • Casey:

        • Well, I guess the thing I would ask is, what is the benefit of making an "object" any more formal than just the struct and some functions? You already have the ability through function overloading to change which random number API you are using by changing the type (random_series to random_series_2 or whatever). So what is the benefit of making an "object" out of it?

        • If the answer is polymorphism, well, yeah, at that point you have a vtable situation and things start to get a lot more expensive, because the random number generation can no longer be inlined, for example - it always has to be a function call. If the answer is something else, what is that something else?

        • Yeah, ML and friends do type inference in a much better way, without making you do all kinds of template nonsense and so on. But there's a whole other set of things you have to worry about if you go that direction. It would have been nice if C++ had introduced a happy medium, but of course they always do the worst possible thing so they didn't :(

          • ML :

            • Stands for MetaLanguage โ€” a family of functional programming languages that includes:

              • Standard ML (SML)

              • OCaml

              • F (influenced by ML, part of the .NET ecosystem)

              • Caml (precursor to OCaml)

            • These languages are known for:

              • Powerful static type systems

              • Type inference: The compiler can deduce the types of most expressions without requiring explicit type annotations.

              • Immutable data structures by default

              • Strong support for pattern matching, algebraic data types, and functional abstractions

          • He is contrasting ML-style type inference (clean, automatic, minimal boilerplate) with C++ templates, which:

            • Often require verbose and complex syntax

            • Have poor error messages

            • Do not integrate cleanly with the rest of the type system

            • Are Turing-complete but hard to control (template metaprogramming)

      • Ginger Bill:

        • I use Go(lang) a lot at work and Go interfaces can be useful. In the io  package, there are a few very useful interfaces: Reader, Writer, ReadWriter, WriterAt, WriterTo, ReaderAt, ReaderFrom.

        • Interfaces are implicit so all you have to do is implement the functions for that type and it will automatically behave as that interface. I don't use interfaces that often as I usually just use structures and functions for most things but they are useful when you need a generic function.

        • I do believe that they are implemented as vtables internally which can be a problem.

        • I know that in C++17, they will/might implement concepts which act very similar but I do not know if they will solve it. I do not know how C++17 concepts are implemented nor have I ever had the chance to use them; so I cannot comment.

VTables (Virtual Tables)

  • A Vtable is:

    • A table of function pointers.

    • Each class with virtual functions has its own vtable.

    • Each object of that class contains a hidden pointer to its classโ€™s vtable (commonly the first pointer in the object's memory layout).

  • When a virtual method is called, the compiler emits code that:

    • Looks up the function pointer in the vtable.

    • Indirectly calls that function through the pointer.

  • Why VTables Can Be Problematic :

    • Performance Overhead

      • No inlining :

        • Virtual function calls can't be inlined because the exact function isn't known at compile time.

        • In C++, Virtual functions disable inlining unless compiler devirtualizes.

      • Indirect branch :

        • Every call goes through an extra pointer dereference, which introduces a pipeline stall or branch prediction failure on modern CPUs.

      • Cache misses :

        • Function pointers may not be in cache, leading to further delays.

    • Hidden Complexity

      • VTables are often invisible in source code in C++. You donโ€™t explicitly write the table โ€” the compiler generates it.

        • Every polymorphic object gets a hidden vtable pointer.

      • This leads to less control and transparency, especially when debugging or optimizing.

      • Harder Debugging

        • Debugging virtual dispatch is more difficult because the function being called isnโ€™t directly visible in code.

        • Tools must inspect vtable pointers and offsets to determine the actual call target.

    • Binary Size and ABI Fragility

      • Every virtual function adds a pointer to the vtable.

      • Changing the vtable layout breaks binary compatibility (ABI), which is a concern in shared library design.

  • Calling functions inside classes via the function address stored in the VTable .

    • This is used to do VTable swapping. Somehow this is used for hacking.